Go 的 Request 结构体解析
Request 结构体
Go 通过一个 Request 结构体来表示 HTTP 请求报文,这个结构体位于内置的 net/http 包中,其中包含了 HTTP 请求的所有信息,包括请求 URL、请求头、请求实体、表单信息等,平时常用的、比较重要的一些字段如下所示:
- URL:请求 URL
- Method:请求方法
- Proto:HTTP 协议版本
- Header:请求头(字典类型的键值对集合)
- Body:请求实体(实现了 io.ReadCloser 接口的只读类型)
- Form、PostForm、MultipartForm:请求表单相关字段,可用于存储表单请求信息
另外还有很多其他字段,比如 Host、From、ContentLength 等,这里就不一一列举了
URL 字段
在 Go 语言的 http.Request 对象中,用于表示请求 URL 的 URL 字段是一个 url.URL 类型的指针:
来简单介绍下其中常见的字段:
- Scheme 表示 HTTP 协议是 HTTPS 还是 HTTP,在上面的例子中是 https;(回环请求是不显示的)
- 对于一些需要认证才能访问的应用,需要提供 User 信息;
- Host 字段表示域名/主机信息,如果服务器监听端口不是默认的 80 端口的话,还会加
:端口号
- Path 表示 HTTP 请求路径,一般应用首页是空字符串,或者
/
; - Query 相关字段表示 URL 中的查询字符串,也就是 URL 中
?
之后的部分; - Fragment 表示 URL 中的锚点信息,也就是 URL 中
#
之后的部分。
注意,如果使用的是 localhost,要取得这个 host 得用 r.HOST
否则 r.URL.Host
可能为空(在本地开发环境中,Host 始终为空)
func index(w http.ResponseWriter, r *http.Request) {
if r.URL.Scheme != "https" {
http.Redirect(w, r, "https://"+r.Host+r.URL.Path, 301)
return
}
//....
}
因此,常见的 URL 完整格式如下:
scheme://[user@]host/path[?query][#fragment]
补充:如果请求是从浏览器发送的话,我们无法获取 URL 中的 Fragment 信息,这不是 Go 的问题,而是浏览器根本没有将其发送到服务端。那为什么还要提供这个字段呢?因为不是所有的请求都是从浏览器发送的,而且 Request 也可以在客户端库中使用。
请求头
请求头和响应头都通过 Header 字段表示,Header 是一个键值对字典,键是字符串,值是字符串切片。Header 提供了增删该查方法用于对请求头进行读取和设置。
要获取某个请求头的值很简单,通过 Header 对象提供的 Get 方法,传入对应的字段名即可,比如要获取请求头中 User-Agent 字段,可以这么做:
r.Header.Get("User-Agent")
此外,我们还可以通过 Header 提供的 Add 方法新增请求头:
r.Header.Add("test", "value1")
通过 Header 提供的 Set 方法修改请求头:
r.Header.Set("test", "value2")
以及通过 Header 提供的 Del 方法删除请求头:
r.Header.Del("test")
请求实体
请求实体和响应实体都通过 Body 字段表示,该字段是 io.ReadCloser
接口类型。顾名思义,这个类型实现了 io.Reader
和 io.Closer
接口。
io.Reader
提供了 Read 方法,用于读取传入的字节切片并返回读取的字节数以及错误信息,io.Closer
提供了 Close 方法,因此,你可以在 Body 上调用 Read 方法读取请求实体的内容,调用 Close 方法释放资源。
对于请求实体来说,对应的 Body 访问路径是 http.Request.Body
,下面编写一段测试代码来演示请求实体的读取,在 goblog/handlers/post.go 中新增一个 AddPost 处理器方法:
func AddPost(w http.ResponseWriter, r *http.Request) {
len := r.ContentLength // 获取请求实体长度
body := make([]byte, len) // 创建存放请求实体的字节切片
r.Body.Read(body) // 调用 Read 方法读取请求实体并将返回内容存放到上面创建的字节切片
io.WriteString(w, string(body)) // 将请求实体作为响应实体返回
}
值得注意,这个 Body 读取后之后,如果想要再次读取,需要把数据塞回去
ioutil.NopCloser(bytes.NewReader([]byte("这是数据")))
读取 Form
Go 也为此提供多个不同的结构体帮助我们读取不同请求类型的数据,首先,我们可以通过请求对象上的 Form 读取所有 GET/POST 请求数据,在 handlers/post.go 中新增 EditPost 方法如下:
func EditPost(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
fmt.Fprintln(w, r.Form)
}
这个 r.Form
就是一个 Map
fmt.Println(r.Form["username"])
需要注意的是,在通过 r.Form
获取所有请求数据之前,必须要先通过 r.ParseForm()
解析所有请求数据,否则无法获取数据。
也可以通过 Form 提供的 Get 方法,就像我们从一个普通字典类型获取键值一样:
id1 := r.Form["id"]
id2 := r.Form.Get("id")
只不过两者的返回值类型不一样,前者是一个字符串切片,后者是一个字符串值:
[1]
1
读取 PostForm
上面的结果同时返回了查询字符串和请求实体,如果只想获取请求实体(即 POST 表单中的数据),可以通过 PostForm 实现:
func EditPost(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
id := r.Form.Get("id")
fmt.Println("post id:", id)
fmt.Println("form data:", r.PostForm)
io.WriteString(w, "表单提交成功")
}
通过 PostForm 获取具体参数值的方式和 Form 一样
FormValue/PostFormValue
最后,还可以通过 FormValue 和 PostFormValue 获取用户请求数据,使用它们的好处是 不再需要单独调用 ParseForm 对表单数据进行解析,不过使用这两个方法的时候只能获取特定请求数据,不能一次获取所有请求数据:
func EditPost(w http.ResponseWriter, r *http.Request) {
fmt.Println("post id:", r.FormValue("id"))
fmt.Println("post title:", r.PostFormValue("title"))
fmt.Println("post title:", r.PostFormValue("content"))
io.WriteString(w, "表单提交成功")
}
FormValue/PostFormValue 的区别和 Form/PostForm 一样,这里通过命名就可以看出来,前者可以获取所有 GET/POST 请求数据(即查询字符串和请求实体),后者只能获取 POST 请求实体数据。
注:FormValue/PostFormValue 之所以不用显式调用 ParseForm 解析请求数据,是因为底层对其进行了封装,实际上还是要调用这个方法。
获取 JSON 请求数据
上面的示例默认都是基于 HTML 表单请求,对于客户端提交的 JSON 格式数据,使用 ParseForm 是无法解析并获取数据的,因为 HTML 表单请求数据默认是通过 application/x-www-form-urlencoded
编码的,而 JSON 请求数据通常是通过 application/json
编码,ParseForm 只能解析通过 application/x-www-form-urlencoded
编码的数据。
json 数据直接使用 io 读取到数据再解析就行了
type Post struct {
Title string `json:"title"`
Content string `json:"content"`
}
func AddPost(w http.ResponseWriter, r *http.Request) {
len := r.ContentLength // 获取请求实体长度
body := make([]byte, len) // 创建存放请求实体的字节切片
r.Body.Read(body) // 调用 Read 方法读取请求实体并将返回内容存放到上面创建的字节切片
// io.WriteString(w, string(body))
post := Post{}
json.Unmarshal(body, &post) // 对读取的 JSON 数据进行解析
fmt.Fprintf(w, "%#v\n", post) // 格式化输出结果
}
MultipartForm 请求
Go 语言为文件类型请求数据提供了单独的请求字段 MultipartForm,它是一个 multipart.Form
类型的指针,要解析并获取这个字段,可以这么做:
func EditPost(w http.ResponseWriter, r *http.Request) {
...
r.ParseMultipartForm(1024)
fmt.Println("post file:", r.MultipartForm)
io.WriteString(w, "表单提交成功")
}
这里,需要在调用 ParseMultipartForm 时传入存储解析后文件的最大内存值(单位是字节)。MultipartForm 包含了所有 POST 表单请求字段,即 PostForm 中的所有内容,但不包含 URL 查询字符串中的请求参数。
MultipartForm 返回的值包含两个部分,一部分是单纯的 POST 请求字段,我们可以通过 Value 字段来访问它,另一部分就是包含文件信息的字典,我么可以通过 File 字段来访问它。
打开 Postman 模拟客户端请求,填写 URL 和 表单字段(数据编码类型选择 form-data
,即 multipart/form-data
):
表单数据设置好了之后,勾选上所有数据,然后点击「Send」发送请求,看到响应实体(Body)中显示「表单提交成功」,表明服务端已经处理完请求并成功返回响应,我们到启动 HTTP 服务器的位置查看服务端日志:
可以看到请求头中的 Content-Type 是 multipart/form-data
,并且通过 r.MultipartForm
成功获取到了 POST 表单数据,包含文件信息(位于一个独立的 map 中)。